// Copyright 2009 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: Gunnar Ritter
//
// Implements a probe to exercise IMAP (RFC 3501) over SSL on port 993.

#include <stdarg.h>
#include <QtCore/QDir>
#include <QtCore/QMap>
#include <QtCore/QRegExp>
#include <QtCore/QTime>
#include <QtNetwork/QtNetwork>
#include <QtGui/QTextDocument>
#include <algorithm>

#include "probes/imapprobe.h"
#include "probes/ssl.h"

namespace popdiag {

const int ImapProbe::kTimeoutMs = 30000;

ImapProbeFactory::ImapProbeFactory(const QString &username,
                                   const QString &password,
                                   const QString &imap_server_name,
                                   bool pull_all_mail)
    : username_(username), password_(password),
    imap_server_name_(imap_server_name), pull_all_mail_(pull_all_mail) {
    }

Probe *ImapProbeFactory::CreateProbe(const QHostAddress &addr,
                                     bool do_complete_probe) {
  return new ImapProbe(username_, password_, imap_server_name_,
                       addr, pull_all_mail_, do_complete_probe);
}

ImapProbe::ImapProbe(const QString &username, const QString &password,
                     const QString &imap_server,
                     const QHostAddress &host_address,
                     bool pull_all_mail, bool do_complete_probe)
: Probe("Imap"), username_(username), password_(password),
      imap_server_(imap_server),
      base_socket_(), socket_(), state_(INIT),
      max_state_(do_complete_probe ? LOGOUT : BANNER),
      fault_(OK), pull_all_mail_(pull_all_mail),
      probe_banner_only_(!do_complete_probe), we_closed_(false),
      bytes_retrieved_(0),
      bytes_expected_(0),
      msgs_in_mailbox_(-1), msgs_retrieved_(0), uids_(),
      lengths_(), target_address_(host_address),
      connect_timer_(NULL), ssl_negotiate_timer_(NULL),
      fetch_timer_(NULL), reader_(NULL),
      tag_(0), tagstring_(""), authenticated_(false), capabilities_(0) {
}

ImapProbe::~ImapProbe() {
}

void ImapProbe::SetState(ProbeState new_state) {
  CHECK_GE(max_state_, new_state);
  state_ = new_state;
  emit ProgressMade(new_state, max_state_);
}

void ImapProbe::GetProgress(int *completed, int *estimated_total) {
  if (state_ == FETCH_CONTENTS && bytes_retrieved_ != 0 &&
      bytes_expected_ != 0) {
    *completed = max_state_ + 10.0 * bytes_retrieved_ / bytes_expected_;
  } else {
    *completed = state_;
  }
  *estimated_total = max_state_ + 10;
}

int ImapProbe::GetFaultId() {
  return fault_;
}

QString ImapProbe::GetDisplayText() {
  switch (fault_) {
    case OK:
      return tr("The IMAP server appears to be working properly "
                "for your account.",
                "The IMAP test succeeded.");
    case RESOLVE_FAILED:
      return tr("The IMAP server does not resolve in DNS. "
                "Is the network connected?",
                "Resolving the hostname for the IMAP Server using the "
                "Domain Name System (DNS) failed. This can be a symptom "
                "that the computer is not connected to the internet.");
    case CONNECTION_REFUSED:
      return tr("The IMAP server refused our connection attempt.",
                "Establishing a TCP connection failed because the "
                "remote server refused to accept the connection.");
    case SERVER_CLOSED_PREMATURELY:
      return tr("The IMAP server closed the connection prematurely",
                "Some time during the IMAP protocol conversation, the server "
                "closed the TCP connection unexpectedly.");
    case SOCKET_ERROR:
      return tr("We were unable to use the network to "
                "connect to the IMAP server.",
                "Using the local Sockets API, we were unable to get a socket "
                "to use to connect to the IMAP server.");
    case IMAP_SERVER_CAFILE_NOT_FOUND:
      // file of acceptable SSL certifying authorities was not found where
      // we expected it.  We cannot evaluate SSL certificate validity.
      return tr("There is a problem with the installation of "
                "this program (CAFile).",
                "A needed configuration file (CAFile) is missing from "
                "the installation.");
    case IMAP_SERVER_CERT_SELFSIGNED:
      return tr("The SSL certificate for the IMAP server is not valid. "
                "It is self-signed, or a certificate in the certifying "
                "authority chain is self-signed.",
                "The SSL certificate presented by the server is unacceptable.");
    case IMAP_SERVER_CERT_EXPIRED:
      return tr("The SSL certificate for the IMAP server is expired.",
                "The SSL certificate presented by the server is unacceptable.");
    case IMAP_SERVER_CERT_NOTYETVALID:
      return tr("The SSL certificate for the IMAP server is not yet valid.",
                "The SSL certificate presented by the server is unacceptable.");
    case IMAP_SERVER_CERT_CANNOT_VALIDATE:
      return tr("We cannot validate the SSL certificate for the IMAP server.",
                "The SSL certificate presented by the server is unacceptable.");
    case IMAP_SERVER_CERT_NAME_MISMATCH:
      return tr("The SSL certificate for the IMAP server does "
                "not match the hostname we used to connect to it.",
                "The hostname presented in the SSL certificate is different "
                "than the hostname we used to connect to it.");
    case IMAP_SERVER_SHEDDING_LOAD:
      return tr("The IMAP server is overloaded and is turning "
                "away connections.",
                "We can't talk to the IMAP server because it is overloaded "
                "and turned us away.");
    case IMAP_SERVER_RESPONSE_MALFORMED:
      return tr("The IMAP server sent a malformed response to our command.",
                "The IMAP server sent a protocol response to our request "
                "that did not match our expectations.");
    case IMAP_LOGIN_REJECTED:
      return tr("The IMAP server rejected our username or password. "
                "This is generally a configuration problem.",
                "The IMAP Server failed on the LOGIN command.");
    case IMAP_LOGOUT_REJECTED:
      return tr("The IMAP server rejected our LOGOUT command. "
                "This is likely a server problem.",
                "The IMAP Server failed on the USER command.");
    case IMAP_LOGIN_FAILED_TEMP_SYSTEM_FAILURE:
      return tr("The IMAP server could not process the login attempt for "
                "this user due to a temporary system failure. "
                "Please try again later.",
                "The login failed, but a status code indicates that it was "
                "due to a system failure that is expected to resolve itself "
                "in time.");
    case IMAP_NOT_ENABLED_FOR_USER:
      return tr("IMAP has not been enabled for this account "
                "in the web interface.",
                "Gmail IMAP must be enabled as an account preference. "
                "That has not been done for this account.");
    case IMAP_NOT_ENABLED_FOR_DOMAIN:
      return tr("IMAP is disabled for this domain. "
                "Please contact your domain administrator for questions "
                "about this feature.");
    case IMAP_NOT_AVAILABLE_FOR_USER:
      return tr("IMAP is not available for your account.");
    case IMAP_WEB_LOGIN_REQUIRED:
      return tr("IMAP access is temporarily disabled. "
                "You need to login at the web interface first.");
    case IMAP_ACCOUNT_NOT_READY:
      return tr("Your account is not ready for IMAP use. "
                "Please contact support if you continue "
                "to receive this message.");
    case IMAP_LOGIN_FAILED_ABUSE:
      return tr("The IMAP server refused to service this account due to abuse.",
                "The login failed and a status code indicates that the "
                "problem is that the account is flagged for abuse.");
    case IMAP_LOGIN_FAILED_GEO_LOCKOUT:
      return tr("The IMAP server does not offer IMAP service to the part "
                "of the world associated with this IP address.",
                "The login failed and a status code indicates that this "
                "happened because the IMAP service is not offered to the part "
                "of the world the request comes from.");
    case IMAP_LOGIN_FAILED_BAD_CREDS:
      return tr("The IMAP server did not accept your username and password.",
                "The login failed and a status code indicates that the "
                "problem was the username and password presented "
                "did not match.");
    case IMAP_FETCH_STATS_FAILED:
      return tr("The IMAP server failed to return message statistics. "
                "Please contact user support.");
    case IMAP_FETCH_CONTENTS_FAILED:
      return tr("The IMAP server would not return a "
                "requested message from your account.",
                "Login succeeded, but retrieving a message failed.");
    case IMAP_SELECT_FAILED:
      return tr("The IMAP server rejected our request to select the inbox.",
                "This indicates a server problem. "
                "Please contact user support.");
    case CONNECT_TIMED_OUT:
      return tr("Connecting to the IMAP Server timed out.",
                "Establishing a TCP connection to the IMAP server failed "
                "to complete in a timely manner. "
                "This is most likely a problem with your network connection, "
                "your service provider, or the downstream network.");
    case IMAP_RESPONSE_TIMED_OUT:
      return tr("The IMAP server did not return a response to our command.",
                "A command timed out before receiving a response "
                "from the server.");
    case UNKNOWN:
      // fall through to default handling
      ;
  }
  CHECK(false);
  return tr("An unknown error occurred.", "This should NEVER happen.");
}

void ImapProbe::StartProbe() {
  ClearLog();
  StartLogDetails();
  StartProbeConnect();
}

void ImapProbe::Cleanup() {
  we_closed_ = true;
  if (base_socket_ != NULL) {
    // Do NOT delete the socket.  Just close it.
    // It's still in use on the stack below us, in all probability.
    base_socket_->close();
  }
}

void ImapProbe::ProbeDone(ImapProbeFault f) {
  if (we_closed_ || fault_ != OK) {
    return;
  }
  Cleanup();

  // First fault wins.
  EndLogDetails();
  fault_ = f;
  emit ProbeComplete(fault_ == OK ? PROBE_OK : PROBE_FAILED);
}

void ImapProbe::StartProbeConnect() {
  SetState(CONNECT);

  // Set a connection failure timeout
  connect_timer_ = new QTimer(this);
  connect(connect_timer_, SIGNAL(timeout()),
          this, SLOT(OnConnectTimedOut()));
  connect_timer_->setSingleShot(true);
  connect_timer_->start(kTimeoutMs);

  base_socket_.reset(new RenamableSslSocket());
  CHECK(connect(base_socket_.get(),
                SIGNAL(error(QAbstractSocket::SocketError)),
                this, SLOT(OnConnectError(QAbstractSocket::SocketError))));
  CHECK(connect(base_socket_.get(), SIGNAL(connected()),
                this, SLOT(OnConnected())));
  CHECK(connect(base_socket_.get(), SIGNAL(disconnected()),
                this, SLOT(OnDisconnected())));
  socket_ = base_socket_.get();
  int port = 993;
  LogDetails("AttemptConnect", true, QString("%1:%2")
             .arg(target_address_.toString()).arg(port));
  base_socket_->connectToHost(target_address_, port);
}

void ImapProbe::OnConnected() {
  connect_timer_->stop();
  LogDetails("Connect", true, "");
  // Socket is currently "named" by its IP address.  SSL cert validation
  // will fail with a name match.  That's why we used a Renamable
  // TCP Socket.  So replace the name out from under it with the
  // DNS name.
  base_socket_->SetDnsName(imap_server_);
  StartProbeNegotiateSsl();
}

void ImapProbe::OnDisconnected() {
  connect_timer_->stop();
  if (state_ != LOGOUT && base_socket_->bytesAvailable() == 0) {
    LogDetails("Disconnected", false, "???");
    ProbeDone(SERVER_CLOSED_PREMATURELY);
  }
}

void ImapProbe::OnConnectTimedOut() {
  LogDetails("Connect", false, "TCP Connect timed out");
  ProbeDone(CONNECT_TIMED_OUT);
}

void ImapProbe::OnConnectError(QAbstractSocket::SocketError err) {
  connect_timer_->stop();
  switch (err) {
    case QAbstractSocket::ConnectionRefusedError:
      LogDetails("Connect", false, "Connection refused");
      ProbeDone(CONNECTION_REFUSED);
      break;
    case QAbstractSocket::RemoteHostClosedError:
      if (state_ != LOGOUT && base_socket_->bytesAvailable() == 0) {
        LogDetails("Connect", false,
                    "Server closed the connection before the banner");
        ProbeDone(SERVER_CLOSED_PREMATURELY);
      }
      break;
    case QAbstractSocket::HostNotFoundError:
      LogDetails("Connect", false,
                  "Connect with IP parameter failed with a resolve error?");
      ProbeDone(RESOLVE_FAILED);
      break;
    case QAbstractSocket::SocketTimeoutError:
      LogDetails("Connect", false, "TCP Connect timed out");
      ProbeDone(CONNECT_TIMED_OUT);
      break;
    case QAbstractSocket::SocketAccessError:
    case QAbstractSocket::SocketResourceError:
    case QAbstractSocket::NetworkError:
    case QAbstractSocket::UnsupportedSocketOperationError:
    case QAbstractSocket::UnknownSocketError:
    default:
      QString error = "no socket";
      if (base_socket_ != NULL) {
        error = base_socket_->errorString();
      }
      LogDetails("Connect", false,
                 QString("Failed with code %1 (%2)")
                        .arg(err).arg(error));
      ProbeDone(SOCKET_ERROR);
  }
}

void ImapProbe::StartProbeNegotiateSsl() {
  SetState(NEGOTIATE_SSL);
  QDir dir = QDir::current();
  if (!SetCACertificates(base_socket_.get())) {
    LogDetails("SSL", false, "Trusted CA file not found");
    ProbeDone(IMAP_SERVER_CAFILE_NOT_FOUND);
    return;
  }
  CHECK(connect(base_socket_.get(),
                SIGNAL(sslErrors(const QList<QSslError> &)),
                this,
                SLOT(OnSslError(const QList<QSslError> &))));
  CHECK(connect(base_socket_.get(), SIGNAL(encrypted()),
                this, SLOT(OnSslEstablished())));
  CHECK(connect(base_socket_.get(),
                SIGNAL(error(QAbstractSocket::SocketError)),
                this,
                SLOT(OnError(QAbstractSocket::SocketError))));

  ssl_negotiate_timer_ = new QTimer(this);
  CHECK(connect(ssl_negotiate_timer_, SIGNAL(timeout()),
                this, SLOT(OnSslTimeout())));
  ssl_negotiate_timer_->setSingleShot(true);
  ssl_negotiate_timer_->start(kTimeoutMs);

  base_socket_->startClientEncryption();
}

void ImapProbe::OnError(QAbstractSocket::SocketError err) {
  // In some scenarios, the server sends a response then drops us.
  // In such cases, sometimes we get the OnError first, sometimes we
  // get the DataAvailable first.  Give the app the chance to
  // fire app-level errors before we demote it to the less-specific
  // socket-level error.
  if (reader_) {
    reader_->TryRead();
  }

  CHECK(state_ != CONNECT);
  QString error_string = base_socket_->errorString();
  base_socket_->close();
  if (we_closed_ || fault_ != OK) {
    return;
  }
  LogDetails(StepNameFromState(), false,
             QString("SocketError %1: %2").arg(err).arg(error_string));
  if (state_ == FETCH_CONTENTS) {
    LogFetchContentsFailure(NULL);
  }
}

void ImapProbe::OnSslTimeout() {
  LogDetails("SSL", true, "Negotiation timed out");
  ProbeDone(IMAP_SERVER_CERT_CANNOT_VALIDATE);
  Cleanup();
}

void ImapProbe::OnSslError(const QList<QSslError> &errors) {
  ssl_negotiate_timer_->stop();
  QString debug = base_socket_->peerCertificate().toPem();
  // TODO: Handle all errors of this list
  switch (errors[0].error()) {
    case QSslError::NoError:
      // This shouldn't ever happen, as this function would
      // not have been called in this case.
      LogDetails("SSL", true, "Socket secured");
      socket_ = base_socket_.get();
      break;
    case QSslError::HostNameMismatch:
      LogDetails("SSL", false, "Hostname on cert does not match: " + debug);
      ProbeDone(IMAP_SERVER_CERT_NAME_MISMATCH);
      break;
    case QSslError::SelfSignedCertificate:
    case QSslError::SelfSignedCertificateInChain:
      LogDetails("SSL", false, "Found self signed cert in cert chain: "
                 + debug);
      ProbeDone(IMAP_SERVER_CERT_SELFSIGNED);
      break;
    case QSslError::CertificateExpired:
      LogDetails("SSL", false, "Found expired certificate in cert chain: "
                 + debug);
      ProbeDone(IMAP_SERVER_CERT_EXPIRED);
      break;
    case QSslError::CertificateNotYetValid:
      LogDetails("SSL", false,
                 "Found not yet valid certificate in cert chain: " + debug);
      ProbeDone(IMAP_SERVER_CERT_NOTYETVALID);
      break;
    case QSslError::UnableToVerifyFirstCertificate:
    default:
      // TODO: Complete the defined SslError cases.
      LogDetails("SSL", false, "Unable to validate cert: " + debug);
      ProbeDone(IMAP_SERVER_CERT_CANNOT_VALIDATE);
      break;
  }
}

void ImapProbe::OnSslEstablished() {
  ssl_negotiate_timer_->stop();
  BeginSecureConversation();
}

void ImapProbe::BeginSecureConversation() {
  StartProbeBanner();
}

ImapResponseReader *ImapProbe::GetReader() {
  reader_ = new ImapResponseReader(this, socket_, kTimeoutMs);
  connect(reader_, SIGNAL(ResponseRead(ImapResponseReader *)),
          this, SLOT(OnCommandResponseReceived(ImapResponseReader *)));
  return reader_;
}

void ImapProbe::StartProbeBanner() {
  SetState(BANNER);
  SendCommand("");
}

const char *ImapProbe::StepNameFromState() {
  switch (state_) {
    case INIT:
      return "INIT";
    case CONNECT:
      return "CONNECT";
    case NEGOTIATE_SSL:
      return "NEGOTIATE_SSL";
    case BANNER:
      return "BANNER";
    case CAPABILITY:
      return "CAPABILITY";
    case LOGIN:
      return "LOGIN";
    case SELECT:
      return "SELECT";
    case FETCH_STATS:
      return "FETCH_STATS";
    case FETCH_CONTENTS:
      return "FETCH_CONTENTS";
    case LOGOUT:
      return "LOGOUT";
    default:
      return "???";
  }
}

void ImapProbe::OnCommandResponseReceived(ImapResponseReader *reader) {
  if (we_closed_) {
    return;
  }
  QString fault_name = "???";
  ImapProbeFault code = IMAP_SERVER_RESPONSE_MALFORMED;
  bool terminate = true;
  switch (reader->status_response()) {
    case IMAP_STATUS_RESPONSE_OK:
    case IMAP_STATUS_RESPONSE_NO:
    case IMAP_STATUS_RESPONSE_BYE:
    case IMAP_STATUS_RESPONSE_PREAUTH:
    case IMAP_STATUS_RESPONSE_CAPABILITY:
    case IMAP_STATUS_RESPONSE_EXPUNGE:
    case IMAP_STATUS_RESPONSE_FETCH:
    case IMAP_STATUS_RESPONSE_FLAGS:
    case IMAP_STATUS_RESPONSE_LIST:
    case IMAP_STATUS_RESPONSE_LSUB:
    case IMAP_STATUS_RESPONSE_SEARCH:
    case IMAP_STATUS_RESPONSE_STATUS:
    case IMAP_STATUS_RESPONSE_EXISTS:
    case IMAP_STATUS_RESPONSE_RECENT:
      terminate = false;
      break;
    case IMAP_STATUS_RESPONSE_BAD:
      fault_name = "Bad";
      break;
    case IMAP_STATUS_RESPONSE_UNKNOWN:
      fault_name = "Unknown";
      break;
    case IMAP_STATUS_RESPONSE_MALFORMED:
      fault_name = "Malformed";
      code = IMAP_SERVER_RESPONSE_MALFORMED;
      break;
    case IMAP_SOCKET_DROPPED:
      fault_name = "Dropped";
      code = SERVER_CLOSED_PREMATURELY;
      break;
    case IMAP_STATUS_RESPONSE_TIMEOUT:
      fault_name = "Timeout";
      code = IMAP_RESPONSE_TIMED_OUT;
      break;
    default:
      fault_name = QString("Unknown error %1")
        .arg(reader->status_response());
      code = IMAP_SERVER_RESPONSE_MALFORMED;
  }
  if (terminate) {
    if (state_ == FETCH_CONTENTS) {
      LogFetchContentsFailure(reader);
    } else {
      LogDetails(StepNameFromState(), false,
                 fault_name + ": " + reader->describe());
    }
    ProbeDone(code);
    return;
  }
  switch (reader->status_response()) {
    case IMAP_STATUS_RESPONSE_BYE:
      LogDetails("BYE", state_ == LOGOUT,
                 QString("Server will close the connection: %1")
                 .arg(reader->text()));
      if (state_ != LOGOUT)
        ProbeDone(SERVER_CLOSED_PREMATURELY);
      return;
    case IMAP_STATUS_RESPONSE_PREAUTH:
      authenticated_ = true;
      break;
    case IMAP_STATUS_RESPONSE_CAPABILITY:
      LogDetails("CAPABILITY", true, QString("Capabilities advertised: %1")
                 .arg(reader->text()));
      capabilities_ = reader->capabilities();
      break;
    case IMAP_STATUS_RESPONSE_EXPUNGE:
      ProcessExpungeResponse(reader);
      break;
    case IMAP_STATUS_RESPONSE_FETCH:
      ProcessFetchResponse(reader);
      break;
    case IMAP_STATUS_RESPONSE_EXISTS:
      LogDetails("EXISTS", true, QString("Mailbox has %1 msgs")
              .arg(reader->number()));
      msgs_in_mailbox_ = reader->number();
      break;
    case IMAP_STATUS_RESPONSE_RECENT:
      LogDetails("RECENT", true, QString("Mailbox has %1 recent msgs")
              .arg(reader->number()));
      break;
    default:
      {};
  }
  switch (reader->response_code()) {
    case IMAP_RESPONSE_CODE_ALERT:
      LogDetails("ALERT", reader->status_response() != IMAP_STATUS_RESPONSE_NO,
                 QString("Alert received: %1")
                 .arg(reader->text()));
      break;
    case IMAP_RESPONSE_CODE_PARSE:
      LogDetails("PARSE", false, QString("Server failed to parse the headers "
                                        "of a message: %1")
                 .arg(reader->text()));
      break;
    case IMAP_RESPONSE_CODE_UNSEEN:
      LogDetails("UNSEEN", true, QString("Mailbox has %1 unseen msgs")
              .arg(reader->number()));
      msgs_unseen_ = reader->number();
      break;
    case IMAP_RESPONSE_CODE_MALFORMED:
      LogDetails(StepNameFromState(), false,
                 QString("Malformed response code: %1")
                 .arg(reader->first_response_line()));
      break;
    case IMAP_RESPONSE_CODE_UNKNOWN:
      LogDetails(StepNameFromState(), false,
                 QString("Unknown response code: %1")
                 .arg(reader->first_response_line()));
      break;
    case IMAP_RESPONSE_CODE_NONE:
    case IMAP_RESPONSE_CODE_BADCHARSET:
    case IMAP_RESPONSE_CODE_CAPABILITY:
    case IMAP_RESPONSE_CODE_PERMANENTFLAGS:
    case IMAP_RESPONSE_CODE_READ_ONLY:
    case IMAP_RESPONSE_CODE_READ_WRITE:
    case IMAP_RESPONSE_CODE_TRYCREATE:
    case IMAP_RESPONSE_CODE_UIDNEXT:
    case IMAP_RESPONSE_CODE_UIDVALIDITY:
      // These are responses we are not interested in,
      // so nothing to do.
      {};
  }
  if ((reader->response_type() == IMAP_RESPONSE_TYPE_CONTINUE) ||
      (reader->response_type() == IMAP_RESPONSE_TYPE_UNTAGGED &&
       state_ != BANNER) ||
      (reader->response_type() == IMAP_RESPONSE_TYPE_TAGGED &&
       tagstring_ != reader->tag_received())) {
    if (reader->response_type() == IMAP_RESPONSE_TYPE_TAGGED) {
      Debug("IMAP sent tag %s, received tag %s\n",
            qPrintable(tagstring_),
            qPrintable(reader->tag_received()));
    }
    // Read the next part of the response.
    reader->SendCommand("", "");
    return;
  }
  reader->Cleanup();
  switch (reader->status_response()) {
    case IMAP_STATUS_RESPONSE_PREAUTH:
    case IMAP_STATUS_RESPONSE_OK:
      ProcessOkResponse(reader);
      return;
    case IMAP_STATUS_RESPONSE_NO:
      ProcessErrResponse(reader);
      return;
    default:
      LogDetails(StepNameFromState(), false,
                 QString("Unexpected tagged response that is not OK or NO: ")
                 .arg(reader->first_response_line()));
      ProbeDone(IMAP_SERVER_RESPONSE_MALFORMED);
  }
}

void ImapProbe::ProcessOkResponse(ImapResponseReader *reader) {
  switch (state_) {
    case INIT:            // fall through
    case CONNECT:         // fall through
    case NEGOTIATE_SSL:
      CHECK(false);
      return;
    case BANNER:
      BannerOk(reader);
      return;
    case CAPABILITY:
      CapabilityOk(reader);
      return;
    case LOGIN:
      LoginOk(reader);
      return;
    case SELECT:
      SelectOk(reader);
      return;
    case FETCH_STATS:
      FetchStatsOk(reader);
      return;
    case FETCH_CONTENTS:
      FetchContentsOk(reader);
      return;
    case LOGOUT:
      LogoutOk(reader);
  }
}

void ImapProbe::ProcessErrResponse(ImapResponseReader *reader) {
  switch (state_) {
    case BANNER:
      BannerRejected(reader);  // dispatches error
      return;
    case LOGIN:
      LoginRejected(reader);  // dispatches error
      return;
    case FETCH_CONTENTS:
      // Logging only.  Fall through for error dispatch.
      LogFetchContentsFailure(reader);
      break;
    default:
      // Logging only.  Fall through for error dispatch
      LogDetails(StepNameFromState(), false, "Rejected: " + reader->describe());
      break;
  }
  switch (state_) {
    case SELECT:
      ProbeDone(IMAP_SELECT_FAILED);
      return;
    case FETCH_STATS:
      ProbeDone(IMAP_FETCH_STATS_FAILED);
      return;
    case FETCH_CONTENTS:
      ProbeDone(IMAP_FETCH_CONTENTS_FAILED);
      return;
    case LOGOUT:
      ProbeDone(IMAP_LOGOUT_REJECTED);
      return;
    default:
      ProbeDone(IMAP_SERVER_RESPONSE_MALFORMED);
  }
}

void ImapProbe::ProcessExpungeResponse(ImapResponseReader *reader) {
  if (msgs_in_mailbox_ <= 0 || reader->number() > msgs_in_mailbox_) {
    LogDetails("EXPUNGE", false, QString("Server tells to expunge %1 "
                                         "but only %2 messages in mailbox")
                                 .arg(reader->number())
                                 .arg(msgs_in_mailbox_));
    return;
  }
  LogDetails("EXPUNGE", true, QString("Messages expunged: %1")
             .arg(reader->number()));
  for (int i = reader->number() - 1; i < msgs_in_mailbox_ - 1; ++i) {
    lengths_.replace(i, lengths_.value(i + 1));
    uids_.replace(i, uids_.value(i + 1));
  }
  lengths_.removeAt(msgs_in_mailbox_ - 1);
  uids_.removeAt(msgs_in_mailbox_ - 1);
  --msgs_in_mailbox_;
  if (reader->number() <= msgs_retrieved_)
    --msgs_retrieved_;
}

void ImapProbe::ProcessFetchResponse(ImapResponseReader *reader) {
  if (state_ == FETCH_STATS) {
    lengths_.insert(reader->number(), reader->size());
    bytes_expected_ += reader->size();
    uids_.insert(reader->number(), reader->uid());
  } else if (state_ == FETCH_CONTENTS) {
    LogDetails("FETCH", true, QString("Fetched %1 octets of message %2")
              .arg(reader->stringsize()).arg(reader->number()));
    bytes_retrieved_ += reader->stringsize();
    // Fix incorrect sizes being reported previously.
    if (bytes_retrieved_ > bytes_expected_)
      bytes_expected_ = bytes_retrieved_;
  }
}

void ImapProbe::BannerOk(ImapResponseReader *reader) {
    LogDetails("Banner", true, "Banner OK: " + reader->describe());
    NatCheck(reader);
}

void ImapProbe::BannerRejected(ImapResponseReader *reader) {
  LogDetails("Banner", false, "Shedding Load: " + reader->describe());
  ProbeDone(IMAP_SERVER_SHEDDING_LOAD);
}

void ImapProbe::NatCheck(ImapResponseReader *reader) {
  // If we got a Gmail banner with an IP, check for NAT
  QRegExp re("ready for requests from ([\\d\\.]+)");
  if (re.indexIn(reader->text()) >= 0) {
    QString public_ip = re.cap(1);
    if (!public_ip.isEmpty()) {
      QString private_ip = socket_->localAddress().toString();
      bool natted = public_ip != private_ip;
      if (natted) {
        LogDetails("NATCheck", true, "Address translation found");
      } else {
        LogDetails("NATCheck", true, "Address translation not used");
      }
    } else {
      LogDetails("NATCheck", false, "Cannot perform NAT detection");
    }
  }
  if (probe_banner_only_) {
    ProbeDone(OK);
  } else {
    StartProbeCapability();
  }
}

void ImapProbe::StartProbeCapability() {
  SetState(CAPABILITY);
  SendCommand("CAPABILITY");
}

void ImapProbe::CapabilityOk(ImapResponseReader *reader) {
  if (authenticated_)
    LoginOk(reader);
  else
    StartProbeLogin();
}

void ImapProbe::StartProbeLogin() {
  SetState(LOGIN);
  QString cmd = "LOGIN \"" + username_ + "\" \"" + password_ + "\"";
  SendCommand(cmd);
}

void ImapProbe::LoginOk(ImapResponseReader *reader) {
  LogDetails("LOGIN", true, "Accepted: " + reader->describe());
  authenticated_ = true;
  StartProbeSelect();
}

void ImapProbe::LoginRejected(ImapResponseReader *reader) {
  if (reader->response_code() == IMAP_RESPONSE_CODE_ALERT) {
    QString text = reader->text();
    if (text.contains("Web login required")) {
      LogDetails("LOGIN", false, "Web login required for user");
      ProbeDone(IMAP_WEB_LOGIN_REQUIRED);
    } else if (text.contains("Invalid credentials")) {
      LogDetails("LOGIN", false, "User supplied invalid credentials");
      ProbeDone(IMAP_LOGIN_FAILED_BAD_CREDS);
    } else if (text.contains("Access Denied")) {
      LogDetails("LOGIN", false, "Access denied for user");
      ProbeDone(IMAP_LOGIN_FAILED_GEO_LOCKOUT);
    } else if (text.contains("Your account is not ready for IMAP use")) {
      LogDetails("LOGIN", false, "Account not ready for IMAP");
      ProbeDone(IMAP_ACCOUNT_NOT_READY);
    } else if (text.contains("Your account is not enabled for IMAP use")) {
      LogDetails("LOGIN", false, "User has not enabled IMAP access");
      ProbeDone(IMAP_NOT_ENABLED_FOR_USER);
    } else if (text.contains("IMAP access is disabled for your domain")) {
      LogDetails("LOGIN", false, "IMAP Access denied for domain");
      ProbeDone(IMAP_NOT_ENABLED_FOR_DOMAIN);
    } else if (text.contains("IMAP is not available for your account")) {
      LogDetails("LOGIN", false, "IMAP not available for this user");
      ProbeDone(IMAP_NOT_AVAILABLE_FOR_USER);
    } else if (text.contains("Too many simultaneous connections")) {
      LogDetails("LOGIN", false, "Temporary system failure at IMAP login");
      ProbeDone(IMAP_LOGIN_FAILED_TEMP_SYSTEM_FAILURE);
    } else if (text.contains("Lockdown")) {
      LogDetails("LOGIN", false, "User locked out for abuse");
      ProbeDone(IMAP_LOGIN_FAILED_ABUSE);
    }
  }
  ProbeDone(IMAP_LOGIN_REJECTED);
}

void ImapProbe::StartProbeSelect() {
  SetState(SELECT);
  SendCommand("SELECT \"INBOX\"");
}

void ImapProbe::SelectOk(ImapResponseReader * /*reader*/) {
  LogDetails("SELECT", true, QString("INBOX successfully selected"));
  // Starts timers for retrieve here because we want to repeatedly
  // call StartProbeRetr().
  fetch_timer_ = new QTime();
  fetch_timer_->start();
  msgs_retrieved_ = 0;
  if (msgs_in_mailbox_ > 0) {
    StartProbeFetchStats();
  } else {
    if (msgs_in_mailbox_ < 0) {
      LogDetails("SELECT", false, QString("No EXISTS response received"));
      ProbeDone(IMAP_SERVER_RESPONSE_MALFORMED);
    } else {
      LogDetails("FETCH", true, "No mail to retrieve");
      StartProbeLogout();
    }
  }
}

void ImapProbe::StartProbeFetchStats() {
  SetState(FETCH_STATS);
  QString cmd = "FETCH 1:" + IntToString(msgs_in_mailbox_) +
      " (RFC822.SIZE UID)";
  SendCommand(cmd);
}

void ImapProbe::FetchStatsOk(ImapResponseReader * /*reader*/) {
  LogDetails("FETCH", true, "Message sizes and UIDs successfully retrieved");
  StartProbeFetchContents();
}

void ImapProbe::StartProbeFetchContents() {
  SetState(FETCH_CONTENTS);
  QString cmd = "FETCH " + IntToString(msgs_retrieved_+ 1) + " (BODY.PEEK[])";
  SendCommand(cmd);
}

void ImapProbe::FetchContentsOk(ImapResponseReader * /*reader*/) {
  ScheduleNextFetchContents();
}

void ImapProbe::StartProbeLogout() {
  SetState(LOGOUT);
  SendCommand("LOGOUT");
}

void ImapProbe::LogoutOk(ImapResponseReader *reader) {
  LogDetails("LOGOUT", true, QString("Logged out: %1")
             .arg(reader->text()));
  ProbeDone(OK);
}

// logs more details about what we were retrieving extracted from UIDL
// and LIST commands to help us fix this on the backend.
void ImapProbe::LogFetchContentsFailure(ImapResponseReader *reader) {
  QString msg_desc = QString("Message #%1 of %2, UID %3, size %4")
                     .arg(msgs_retrieved_ + 1)
                     .arg(msgs_in_mailbox_).arg(uids_.value(msgs_retrieved_))
                     .arg(lengths_.value(msgs_retrieved_));
  ImapProbeFault fault = IMAP_SERVER_RESPONSE_MALFORMED;
  QString verb = "????";
  if (!reader) {
    fault = SERVER_CLOSED_PREMATURELY;
    verb = "Socket error";
  } else {
    switch (reader->status_response()) {
      case IMAP_STATUS_RESPONSE_OK:
        // should go to FetchContentsOk(), not here.
        CHECK(false);
        break;
      case IMAP_STATUS_RESPONSE_NO:
        // treat err on FETCH as a malformed response
        fault = IMAP_SERVER_RESPONSE_MALFORMED;
        verb = "Failed";
        break;
      case IMAP_STATUS_RESPONSE_MALFORMED:
      default:
        fault = IMAP_SERVER_RESPONSE_MALFORMED;
        verb = "Malformed";
        break;
      case IMAP_SOCKET_DROPPED:
        fault = SERVER_CLOSED_PREMATURELY;
        verb = "Connection dropped";
        break;
      case IMAP_STATUS_RESPONSE_TIMEOUT:
        fault = IMAP_RESPONSE_TIMED_OUT;
        verb = "Timeout";
        break;
    }
  }
  LogDetails("FETCH", false, QString("%1 for %2: %3")
            .arg(verb).arg(msg_desc)
            .arg(reader == NULL ? "no reader" : reader->describe()));
  ProbeDone(fault);
}

void ImapProbe::ScheduleNextFetchContents() {
  if (!pull_all_mail_) {
    LogDetails("FETCH1ST", true,
                "Fetching first message succeeded.  Not checking the rest");
  }
  emit ProgressMade(bytes_retrieved_, bytes_expected_);

  ++msgs_retrieved_;
  bool fetch_more = pull_all_mail_ && (msgs_in_mailbox_ > msgs_retrieved_);

  if (!fetch_more) {
    int millis = fetch_timer_->elapsed();
    // kilo BITS per second
    double throughput_in_kbps = 0.0;
    if (millis != 0) {
      const int kBitsPerByte = 8;
      const double kMillisPerSecond = 1000.0;
      const double kKilo = 1024.0;
      double seconds_elapsed = millis / kMillisPerSecond;
      double bits_retrieved = kBitsPerByte * bytes_retrieved_;
      double bits_per_second = bits_retrieved / seconds_elapsed;
      throughput_in_kbps = bits_per_second / kKilo;
    }
    LogDetails("FETCH", true,
              QString("Fetched %1 messages totalling %2 bytes in %3 ms "
                      "(%4 kbps estimated)")
              .arg(msgs_retrieved_).arg(bytes_retrieved_).arg(millis)
              // Show throughput with at most 3 digits after the decimal
              .arg(throughput_in_kbps, 0, 'f', 3));

    // Normal termination case for ImapProbe.
    StartProbeLogout();
  } else {
    StartProbeFetchContents();
  }
}

void ImapProbe::SendCommand(const QString &cmd) {
  if (!cmd.isEmpty())
    ++tag_;
  tagstring_ = "T" + QString::number(tag_);
  GetReader()->SendCommand(cmd, tagstring_);
}

ImapResponseReader::ImapResponseReader(QObject *parent,
                                       RenamableSslSocket *src,
                                       int timeout_in_millis)
  : QObject(parent), socket_(src), response_lines_(),
    response_type_(IMAP_RESPONSE_TYPE_MALFORMED),
    status_response_(IMAP_STATUS_RESPONSE_MALFORMED),
    response_code_(IMAP_RESPONSE_CODE_NONE),
    capabilities_(0), flags_(0), number_(-1), uid_(-1), size_(-1),
    stringsize_(-1), reader_(src, timeout_in_millis),
    remaining_octets_in_literal_(0), processing_literal_(false), active_(true) {
  CHECK(connect(&reader_, SIGNAL(DataReady(LineReadResult)),
                this, SLOT(DataAvailable(LineReadResult))));
}

ImapResponseReader::~ImapResponseReader() {
}

void ImapResponseReader::DataAvailable(LineReadResult result) {
  switch (result) {
    case DATA_READ:
      TryRead();
      break;
    case TIMEOUT:
      Done();
      break;
    case SOCKET_DROPPED:
      status_response_ = IMAP_SOCKET_DROPPED;
      Done();
      break;
  }
}

QString ImapResponseReader::describe() {
  QString rval;
  QTextStream r(&rval, QIODevice::WriteOnly);
  // each branch of this switch statement must open a <code> tag:
  switch (status_response_) {
    case IMAP_STATUS_RESPONSE_OK:
      r << "<code>OK ";
      break;
    case IMAP_STATUS_RESPONSE_NO:
      r << "<code>NO ";
      break;
    case IMAP_STATUS_RESPONSE_BAD:
      r << "<code>BAD ";
      break;
    case IMAP_STATUS_RESPONSE_PREAUTH:
      r << "<code>PREAUTH ";
      break;
    case IMAP_STATUS_RESPONSE_BYE:
      r << "<code>BYE ";
      break;
    case IMAP_STATUS_RESPONSE_CAPABILITY:
      r << "<code>CAPABILITY ";
      break;
    case IMAP_STATUS_RESPONSE_EXPUNGE:
      r << "<code>EXPUNGE ";
      break;
    case IMAP_STATUS_RESPONSE_FETCH:
      r << "<code>FETCH ";
      break;
    case IMAP_STATUS_RESPONSE_FLAGS:
      r << "<code>FLAGS ";
      break;
    case IMAP_STATUS_RESPONSE_LIST:
      r << "<code>LIST ";
      break;
    case IMAP_STATUS_RESPONSE_LSUB:
      r << "<code>LSUB ";
      break;
    case IMAP_STATUS_RESPONSE_SEARCH:
      r << "<code>SEARCH ";
      break;
    case IMAP_STATUS_RESPONSE_STATUS:
      r << "<code>STATUS ";
      break;
    case IMAP_STATUS_RESPONSE_EXISTS:
      r << "<code>EXISTS ";
      break;
    case IMAP_STATUS_RESPONSE_RECENT:
      r << "<code>RECENT ";
      break;
    case IMAP_STATUS_RESPONSE_UNKNOWN:
      r << "Unknown: <code>";
      break;
    case IMAP_STATUS_RESPONSE_MALFORMED:
      r << "Malformed: <code>";
      break;
    case IMAP_STATUS_RESPONSE_NOT_APPLICABLE:
      r << "Not applicable: <code>";
      break;
    case IMAP_SOCKET_DROPPED:
      r << "ConnDrop: <code>";
      // Fall out and dump the rest, just in case.
      break;
    case IMAP_STATUS_RESPONSE_TIMEOUT:
      r << "Timeout: <code>";
      // Fall out and dump the rest, just in case
      break;
  }

  // show the first two and the last two lines of the response
  for (int i = 0; i < 2 && i < response_lines_.size(); ++i) {
    if (i > 0) {
      r << "<br>";
    }
    r << Qt::escape(response_lines_[i]);
  }
  if (response_lines_.size() > 4) {
    r << "<br>...\n";
  }
  for (int i = response_lines_.size() - 2; i < response_lines_.size();
        ++i) {
    if (i < 2) {
      continue;
    }
    r << "<br>" << Qt::escape(response_lines_[i]);
  }
  r << "</code>";
  return rval;
}

void ImapResponseReader::Cleanup() {
  reader_.Cleanup();
  reader_.disconnect(this);
  active_ = false;
}

void ImapResponseReader::Done() {
  emit ResponseRead(this);
}

void ImapResponseReader::SendCommand(const QString &cmd, const QString &tag) {
  if (!cmd.isEmpty()) {
    Probe::Debug("IMAP sending line: %s %s\n",
                 qPrintable(tag),
                 qPrintable(cmd));
    QString to_write(tag + " " + cmd + "\r\n");
    socket_->write(qPrintable(to_write));
    socket_->flush();
  } else {
    Probe::Debug("IMAP continues reading from server\n");
  }
  RunWhenEventQueueEmpties(this, SLOT(TryRead()));
}

void ImapResponseReader::TryRead() {
  if (!active_) {
    // This function may be called multiple times because
    // both the timer and the DataReady signal can cause
    // a call to it. For processing literals, however, it
    // is important that the exact same reader is used.
    // Thus, we do not allow TryRead() in any reader for
    // which the surrounding code has called the Cleanup()
    // function already.
    return;
  }
  if (processing_literal_) {
    if (remaining_octets_in_literal_ > 0) {
      ParseRestOfLiteral();
    }
    ParseLineAfterLiteral();
  } else {
    QString line;
    if (reader_.NonblockingRead(&line)) {
      ParseResponse(line);
      if (!processing_literal_)
        Done();
    }
  }
}

void ImapResponseReader::ParseResponse(const QString &line) {
  Probe::Debug("IMAP received line: %s", qPrintable(line));
  first_response_line_ = line;
  lexer_ = ImapLexer(line);
  ImapToken token = lexer_.Get();
  switch (token) {
    case IMAP_TOKEN_PLUS:
      response_type_ = IMAP_RESPONSE_TYPE_CONTINUE;
      status_response_ = IMAP_STATUS_RESPONSE_NOT_APPLICABLE;
      // There is also the possibility of a base64 token, but this
      // cannot conflict with resp-text, so we process that in any case.
      ParseRespText();
      return;
    case IMAP_TOKEN_STAR:
      response_type_ = IMAP_RESPONSE_TYPE_UNTAGGED;
      break;
    default:
      response_type_ = IMAP_RESPONSE_TYPE_TAGGED;
      tag_received_ = lexer_.lvalue();
  }
  token = lexer_.Get();
  switch (token) {
    case IMAP_TOKEN_OK:
      status_response_ = IMAP_STATUS_RESPONSE_OK;
      break;
    case IMAP_TOKEN_PREAUTH:
      status_response_ = IMAP_STATUS_RESPONSE_PREAUTH;
      break;
    case IMAP_TOKEN_NO:
      status_response_ = IMAP_STATUS_RESPONSE_NO;
      break;
    case IMAP_TOKEN_BAD:
      status_response_ = IMAP_STATUS_RESPONSE_BAD;
      break;
    case IMAP_TOKEN_BYE:
      status_response_ = IMAP_STATUS_RESPONSE_BYE;
      break;
    case IMAP_TOKEN_CAPABILITY:
      status_response_ = IMAP_STATUS_RESPONSE_CAPABILITY;
      ParseCapability();
      return;
    default:
      lexer_.Unget();
      // ParseMailboxOrMessageData() will determine if the token
      // is a valid one at this point.
      ParseMailboxOrMessageData();
      return;
  }
  ParseRespText();
}

void ImapResponseReader::ParseRespText() {
  ImapToken token = lexer_.Get();
  if (token != IMAP_TOKEN_LBRACKET) {
    response_code_ = IMAP_RESPONSE_CODE_NONE;
    lexer_.Unget();
    text_ = lexer_;
    return;
  }
  token = lexer_.Get();
  switch (token) {
    case IMAP_TOKEN_ALERT:
      response_code_ = IMAP_RESPONSE_CODE_ALERT;
      token = lexer_.Get();
      break;
    case IMAP_TOKEN_BADCHARSET:
      response_code_ = IMAP_RESPONSE_CODE_BADCHARSET;
      // Ignore the contents of this, we are not using it.
      do {
        token = lexer_.Get();
      } while (token != IMAP_TOKEN_RBRACKET && token != IMAP_TOKEN_EOL);
      break;
    case IMAP_TOKEN_CAPABILITY:
      response_code_ = IMAP_RESPONSE_CODE_CAPABILITY;
      ParseCapability();
      break;
    case IMAP_TOKEN_PARSE:
      response_code_ = IMAP_RESPONSE_CODE_PARSE;
      token = lexer_.Get();
      break;
    case IMAP_TOKEN_PERMANENTFLAGS:
      response_code_ = IMAP_RESPONSE_CODE_PERMANENTFLAGS;
      ParseFlags();
      token = lexer_.Get();
      break;
    case IMAP_TOKEN_READ_ONLY:
      response_code_ = IMAP_RESPONSE_CODE_READ_ONLY;
      token = lexer_.Get();
      break;
    case IMAP_TOKEN_READ_WRITE:
      response_code_ = IMAP_RESPONSE_CODE_READ_WRITE;
      token = lexer_.Get();
      break;
    case IMAP_TOKEN_TRYCREATE:
      response_code_ = IMAP_RESPONSE_CODE_TRYCREATE;
      token = lexer_.Get();
      break;
    case IMAP_TOKEN_UIDNEXT:
      response_code_ = IMAP_RESPONSE_CODE_UIDNEXT;
      token = lexer_.Get();
      if (token == IMAP_TOKEN_NUMBER) {
        number_ = lexer_.nvalue();
        token = lexer_.Get();
      } else {
        response_code_ = IMAP_RESPONSE_CODE_MALFORMED;
      }
      break;
    case IMAP_TOKEN_UIDVALIDITY:
      response_code_ = IMAP_RESPONSE_CODE_UIDVALIDITY;
      token = lexer_.Get();
      if (token == IMAP_TOKEN_NUMBER) {
        number_ = lexer_.nvalue();
        token = lexer_.Get();
      } else {
        response_code_ = IMAP_RESPONSE_CODE_MALFORMED;
      }
      break;
    case IMAP_TOKEN_UNSEEN:
      response_code_ = IMAP_RESPONSE_CODE_UNSEEN;
      token = lexer_.Get();
      if (token == IMAP_TOKEN_NUMBER) {
        number_ = lexer_.nvalue();
        token = lexer_.Get();
      } else {
        response_code_ = IMAP_RESPONSE_CODE_MALFORMED;
      }
      break;
    default:
      response_code_ = IMAP_RESPONSE_CODE_UNKNOWN;
      unknown_ = "[" + lexer_.lvalue();
      while ((token = lexer_.Get()) != IMAP_TOKEN_RBRACKET &&
             token != IMAP_TOKEN_EOL)
        unknown_.append(lexer_.lvalue());
      unknown_.append("]");
  }
  if (token != IMAP_TOKEN_RBRACKET)
    response_code_ = IMAP_RESPONSE_CODE_MALFORMED;
  text_ = lexer_;
}

void ImapResponseReader::ParseCapability() {
  // Recognize a bracket delimiter as this may be part
  // of a resp-text, and it will not hurt otherwise.
  capabilities_ = 0;
  ImapToken token;
  text_ = "";
  while ((token = lexer_.Get()) != IMAP_TOKEN_RBRACKET &&
         token != IMAP_TOKEN_EOL) {
    text_.append(lexer_.lvalue() + " ");
    switch (token) {
      case IMAP_TOKEN_IMAP4:
        capabilities_ |= IMAP_CAPABILITY_IMAP4;
        break;
      case IMAP_TOKEN_IMAP4REV1:
        capabilities_ |= IMAP_CAPABILITY_IMAP4REV1;
        break;
      case IMAP_TOKEN_STARTTLS:
        capabilities_ |= IMAP_CAPABILITY_STARTTLS;
        break;
      case IMAP_TOKEN_AUTH_PLAIN:
        capabilities_ |= IMAP_CAPABILITY_AUTH_PLAIN;
        break;
      case IMAP_TOKEN_LOGINDISABLED:
        capabilities_ |= IMAP_CAPABILITY_LOGINDISABLED;
      default:
        // Ignore unknown capabilities, as the IMAP standard forbids
        // them to have any effect unless explicity activated.
        {};
    }
  }
  // remove the trailing space
  text_.chop(1);
}

void ImapResponseReader::ParseMailboxOrMessageData() {
  ImapToken token = lexer_.Get();
  switch (token) {
    case IMAP_TOKEN_FLAGS:
      status_response_ = IMAP_STATUS_RESPONSE_FLAGS;
      ParseFlags();
      break;
    case IMAP_TOKEN_LIST:
      status_response_ = IMAP_STATUS_RESPONSE_LIST;
      // We do not issue LIST, so no further processing done here.
      break;
    case IMAP_TOKEN_LSUB:
      status_response_ = IMAP_STATUS_RESPONSE_LSUB;
      // We do not issue LSUB, so no further processing done here.
      break;
    case IMAP_TOKEN_SEARCH:
      status_response_ = IMAP_STATUS_RESPONSE_SEARCH;
      // We do not issue LIST, so no further processing done here.
      break;
    case IMAP_TOKEN_STATUS:
      status_response_ = IMAP_STATUS_RESPONSE_STATUS;
      // We do not issue STATUS, so no further processing done here.
      break;
    case IMAP_TOKEN_NUMBER:
      number_ = lexer_.nvalue();
      token = lexer_.Get();
      switch (token) {
        case IMAP_TOKEN_EXISTS:
          status_response_ = IMAP_STATUS_RESPONSE_EXISTS;
          break;
        case IMAP_TOKEN_RECENT:
          status_response_ = IMAP_STATUS_RESPONSE_RECENT;
          break;
        case IMAP_TOKEN_EXPUNGE:
          status_response_ = IMAP_STATUS_RESPONSE_EXPUNGE;
          break;
        case IMAP_TOKEN_FETCH:
          ParseFetch();
          break;
        default:
          status_response_ = IMAP_STATUS_RESPONSE_UNKNOWN;
      }
      break;
    default:
      status_response_ = IMAP_STATUS_RESPONSE_UNKNOWN;
  }
}

void ImapResponseReader::ParseFetch() {
  status_response_ = IMAP_STATUS_RESPONSE_FETCH;
  ImapToken token = lexer_.Get();
  if (token == IMAP_TOKEN_LPAREN) {
    stringsize_ = 0;
    while ((token = lexer_.Get()) != IMAP_TOKEN_RPAREN &&
           token != IMAP_TOKEN_EOL) {
      switch (token) {
        case IMAP_TOKEN_FLAGS:
          ParseFlags();
          break;
        case IMAP_TOKEN_STRING:
          stringsize_ += lexer_.lvalue().length();
          response_lines_.append(lexer_.lvalue());
          break;
        case IMAP_TOKEN_LANGLE:
          ParseLiteral();
          return;
        case IMAP_TOKEN_RFC822_SIZE:
          token = lexer_.Get();
          if (token != IMAP_TOKEN_NUMBER) {
            status_response_ = IMAP_STATUS_RESPONSE_MALFORMED;
            return;
          }
          size_ = lexer_.nvalue();
          break;
        case IMAP_TOKEN_UID:
          token = lexer_.Get();
          if (token != IMAP_TOKEN_NUMBER) {
            status_response_ = IMAP_STATUS_RESPONSE_MALFORMED;
            return;
          }
          uid_ = lexer_.nvalue();
          break;
        case IMAP_TOKEN_BODY:
          if ((token = lexer_.Get()) == IMAP_TOKEN_LBRACKET &&
              (token = lexer_.Get()) == IMAP_TOKEN_RBRACKET)
            break;
          // Fall through
        default:
          status_response_ = IMAP_STATUS_RESPONSE_UNKNOWN;
          return;
      }
    }
  } else {
    status_response_ = IMAP_STATUS_RESPONSE_MALFORMED;
  }
}

void ImapResponseReader::ParseFlags() {
  ImapToken token;
  while ((token = lexer_.Get()) != IMAP_TOKEN_RPAREN &&
         token != IMAP_TOKEN_EOL) {
    switch (token) {
      case IMAP_TOKEN_FLAG_ANSWERED:
        flags_ |= IMAP_FLAG_ANSWERED;
        break;
      case IMAP_TOKEN_FLAG_FLAGGED:
        flags_ |= IMAP_FLAG_FLAGGED;
        break;
      case IMAP_TOKEN_FLAG_DELETED:
        flags_ |= IMAP_FLAG_DELETED;
        break;
      case IMAP_TOKEN_FLAG_SEEN:
        flags_ |= IMAP_FLAG_SEEN;
        break;
      case IMAP_TOKEN_FLAG_DRAFT:
        flags_ |= IMAP_FLAG_DRAFT;
        break;
      case IMAP_TOKEN_FLAG_RECENT:
        flags_ |= IMAP_FLAG_RECENT;
        break;
      default:
        {};
    }
  }
}

void ImapResponseReader::ParseLiteral() {
  ImapToken token = lexer_.Get();
  if (token != IMAP_TOKEN_NUMBER) {
    status_response_ = IMAP_STATUS_RESPONSE_MALFORMED;
    return;
  }
  Probe::Debug("IMAP processing literal with %d octets\n", lexer_.nvalue());
  processing_literal_ = true;
  remaining_octets_in_literal_ = lexer_.nvalue();
  stringsize_ = lexer_.nvalue();
  ParseRestOfLiteral();
}

void ImapResponseReader::ParseRestOfLiteral() {
  while (remaining_octets_in_literal_ > 0) {
    QString line;
    if (!reader_.NonblockingRead(&line))
      break;
    // describe() will only need the first few and the last two
    // lines of a message. In order to save memory, do not keep
    // the entire message here, but only a few lines.
    if (response_lines_.size() == 10) {
      response_lines_.insert(9, response_lines_.value(10));
      response_lines_.insert(10, line);
    } else {
      response_lines_.append(line);
    }
    remaining_octets_in_literal_ -= line.length();
    Probe::Debug("IMAP read line: %s", qPrintable(line));
    Probe::Debug("IMAP got %d octets of literal, %d remaining\n", line.length(),
          remaining_octets_in_literal_);
  }
}

void ImapResponseReader::ParseLineAfterLiteral() {
  QString line;
  if (reader_.NonblockingRead(&line)) {
    // There is nothing interesting here, as the only place
    // where we process a literal is when reading message data.
    Probe::Debug("IMAP discarding line: %s", qPrintable(line));
    processing_literal_ = false;
    Done();
  }
}

ImapToken ImapLexer::Get() {
  ImapToken token;
  if (startsWith(' '))
    remove(0, 1);
  if (startsWith('\r') || length() == 0) {
    Probe::Debug("IMAP lexer: at end of line\n");
    return IMAP_TOKEN_EOL;
  } else if (startsWith('+')) {
    token = IMAP_TOKEN_PLUS;
  } else if (startsWith('*')) {
    token = IMAP_TOKEN_STAR;
  } else if (startsWith('[')) {
    token = IMAP_TOKEN_LBRACKET;
  } else if (startsWith(']')) {
    token = IMAP_TOKEN_RBRACKET;
  } else if (startsWith('(')) {
    token = IMAP_TOKEN_LPAREN;
  } else if (startsWith(')')) {
    token = IMAP_TOKEN_RPAREN;
  } else if (startsWith('{')) {
    token = IMAP_TOKEN_LANGLE;
  } else if (startsWith('}')) {
    token = IMAP_TOKEN_RANGLE;
  } else if (startsWith('"')) {
    int length = indexOf(QRegExp("[^\\](\\{2})*\""), 0);
    if (length < 0) {
      Probe::Debug("IMAP lexer: malformed token");
      return IMAP_TOKEN_MALFORMED;
    }
    remove(0, 1);
    lvalue_ = left(length);
    remove(0, length);
    Probe::Debug("IMAP lexer: got \"%s\"\n", qPrintable(lvalue_));
    return IMAP_TOKEN_STRING;
  } else {
    const struct {
      const char *string;
      ImapToken token;
    } stringtokenmap[] = {
      { "OK", IMAP_TOKEN_OK },
      { "PREAUTH", IMAP_TOKEN_PREAUTH },
      { "NO", IMAP_TOKEN_NO },
      { "BAD", IMAP_TOKEN_BAD },
      { "BYE", IMAP_TOKEN_BYE },
      { "CAPABILITY", IMAP_TOKEN_CAPABILITY },
      { "ALERT", IMAP_TOKEN_ALERT },
      { "BADCHARSET", IMAP_TOKEN_BADCHARSET },
      { "PARSE", IMAP_TOKEN_PARSE },
      { "PERMANENTFLAGS", IMAP_TOKEN_PERMANENTFLAGS },
      { "READ-ONLY", IMAP_TOKEN_READ_ONLY },
      { "READ-WRITE", IMAP_TOKEN_READ_WRITE },
      { "TRYCREATE", IMAP_TOKEN_TRYCREATE },
      { "UIDNEXT", IMAP_TOKEN_UIDNEXT },
      { "UIDVALIDITY", IMAP_TOKEN_UIDVALIDITY },
      { "UNSEEN", IMAP_TOKEN_UNSEEN },
      { "FLAGS", IMAP_TOKEN_FLAGS },
      { "LIST", IMAP_TOKEN_LIST },
      { "LSUB", IMAP_TOKEN_LSUB },
      { "SEARCH", IMAP_TOKEN_SEARCH },
      { "STATUS", IMAP_TOKEN_STATUS },
      { "EXISTS", IMAP_TOKEN_EXISTS },
      { "RECENT", IMAP_TOKEN_RECENT },
      { "EXPUNGE", IMAP_TOKEN_EXPUNGE },
      { "FETCH", IMAP_TOKEN_FETCH },
      { "RFC822.SIZE", IMAP_TOKEN_RFC822_SIZE },
      { "UID", IMAP_TOKEN_UID },
      { "BODY", IMAP_TOKEN_BODY },
      { "IMAP4", IMAP_TOKEN_IMAP4 },
      { "IMAP4rev1", IMAP_TOKEN_IMAP4REV1 },
      { "STARTTLS", IMAP_TOKEN_STARTTLS },
      { "AUTH=PLAIN", IMAP_TOKEN_AUTH_PLAIN },
      { "LOGINDISABLED", IMAP_TOKEN_LOGINDISABLED },
      { "\\Answered", IMAP_TOKEN_FLAG_ANSWERED },
      { "\\Flagged", IMAP_TOKEN_FLAG_FLAGGED },
      { "\\Deleted", IMAP_TOKEN_FLAG_DELETED },
      { "\\Seen", IMAP_TOKEN_FLAG_SEEN },
      { "\\Draft", IMAP_TOKEN_FLAG_DRAFT },
      {"\\Recent", IMAP_TOKEN_FLAG_RECENT },
      { NULL, IMAP_TOKEN_OTHER }
    };
    // A simple strpbrk() would be nice to have
    int length = indexOf(QRegExp("[][(){} \r\n]"), 0);
    lvalue_ = left(length);
    remove(0, length);
    int i;
    for (i = 0; stringtokenmap[i].string; i++) {
      if (lvalue_ == stringtokenmap[i].string) {
        token = stringtokenmap[i].token;
        break;
      }
    }
    if (stringtokenmap[i].string == NULL) {
      if (lvalue_.IsNumeric()) {
        token = IMAP_TOKEN_NUMBER;
        nvalue_ = lvalue_.toLongLong();
      } else {
        token = IMAP_TOKEN_OTHER;
      }
    }
    Probe::Debug("IMAP lexer: got %s\n", qPrintable(lvalue_));
    return token;
  }
  lvalue_ = left(1);
  remove(0, 1);
  Probe::Debug("IMAP lexer: got %s\n", qPrintable(lvalue_));
  return token;
}

void ImapLexer::Unget() {
  prepend(lvalue_);
}

bool ImapWord::IsNumeric() const {
  bool ok;
  // toLongLong() returns zero if this is not a number, and it is not
  // completely clear if it would guarantee to set "ok" if it is, so
  // test for both.
  return toLongLong(&ok) != 0 || ok == true;
}

bool ImapWord::operator==(const QString &other) const {
  return compare(other, Qt::CaseInsensitive) == 0;
}

bool ImapWord::operator!=(const QString &other) const {
  return compare(other, Qt::CaseInsensitive) != 0;
}

ImapWord ImapWord::operator=(const char *str) {
  *this = fromAscii(str);
  return *this;
}

ImapWord ImapWord::operator=(QString str) {
  QString::operator=(str);
  return *this;
}

}  // namespace popdiag
